from data import Execution, Action, createDb, Colour, Base
import unittest
from sqlalchemy.engine import create_engine
from sqlalchemy.orm.session import sessionmaker
from config import Configuration
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
from pprint import pprint

class Temporel(object):
    def __init__(self, Session):
        self.Session = Session    

    def startExecution(self, executionId=None ): return Execution.start(self.Session, executionId)

    def reviveExecution(self, executionId): 
        with Execution.committing(self.Session, executionId) as (session, execution): #@UnusedVariable
            if execution.status == Execution.Status.zombie:
                execution.status = Execution.Status.running
            else:
                raise Exception("Only zombies can be revived!")

    def backout(self, executionId=None ): 
        with Execution.committing(self.Session, executionId) as (session, execution): #@UnusedVariable
            execution.backout()
            
    def declare(self, executionId, colourName, **kwargs):
        """ Declare a token involving it in the execution.
        
            @param executionId: ID of the execution
            @param colourName: The name of the colour of the token
            @keyword kwargs: Keyword arguments specifying the fields of the declared token. For foreign
                                keys the handle (the internal db id) of the referred object must be specified.

            If a token with matching colour and fields is found, it is involved in the execution.
            If no such token exists, then one will be created and involved.
            
            @return: A handle (the internal db id) of the involved token.
            @raise MultipleResultsFound: If multiple tokens with matching fields are found.
            @raise DbIntegrityError: A handle provided is invalid (refers to a non-existing object).

        """
        with Execution.committing(self.Session, executionId) as (session, execution):
            colour = session.query(Colour).filter_by(name=colourName).one()
            query = session.query(colour.Entity).filter_by(colour_id=colour.id)
            constructorArgs = dict()
            for key, value in kwargs.items():
                if colour.fields[key].referencedColour : key = Base.foreignKeyName(key)
                constructorArgs[key] = value
                query = query.filter_by(**{key:value})
            try:
                token =  query.one()
            except MultipleResultsFound as e:
                e.args = "Ambiguous {colourName} '{knotId}'".format(**locals()), 
                raise
            except NoResultFound as e:
                token = colour.Entity(colour=colour, **constructorArgs)
            execution.scope.add(token)
            return token.id
                
    def getClearance(self, executionId, actionName):
        """ @return: a dictionary of handle - mapping pairs; 
                    each mapping is a dictionary by itself and assigns 
                    token values to arc names  
                    
                    {'07ad21e95d78': {u'ip': u'1.2.3.4',
                                      u'ip_on_node': u'1.2.3.4',
                                      u'node': u'host123',
                                      u'subnet_on_node': u'1.2.3/24'}} 
                    
            @raise Execution.TryLater: 
        """
        with Execution.committing(self.Session, executionId) as (session, execution):
            if execution.status != Execution.Status.running:
                raise Exception("Execution {executionId} is in status {execution.status}")
            try:
                action = session.query(Action).filter_by(name=actionName).one()
            except NoResultFound:
                raise Exception("Unknown action '{actionName}'".format(**locals()))
            #print action.bindingQuery
            return action.fire(execution)

    def coordinate(self, executionId, outcomes):
        """ @param outcomes: a dictionary assigning the outcome of the action performed
                            on the mapping to the handle of the mapping
                            
                            ('Done', ['07ad21e95d78'])
            @return:  
                      ( 'undo',      
                        [( 'declareIpOnNode',
                           {'07ad21e95d78': {u'ip': u'1.2.3.4',
                                             u'ip_on_node': u'1.2.3.4',
                                             u'node': u'host123',
                                             u'subnet_on_node': u'1.2.3/24'}}),
                         ( 'createSubnetOnNode',
                           {'5162dd43b4dfa': {u'node': u'host123',
                                              u'subnet_on_node': u'1.2.3/24'}}),
                         ]
                       )
        """ 
        with Execution.committing(self.Session, executionId) as (session, execution): #@UnusedVariable
            status, handles = outcomes[0], outcomes[1] 
            if   execution.status == Execution.Status.running:
                if status == "Done": execution.handleDone(handles)
                else: raise Exception("Execution status is 'running', outcome status should be 'Done'.")
                # fire the actions that became enabled in the scope of the execution
            elif execution.status == Execution.Status.backout:
                if status == "Undone": execution.handleUndone(handles)
                else: raise Exception("Execution status is 'backout', outcome status should be 'Undone'.")
                # get and mark more undo-able substitutions
                for substitution in execution.substitutions:
                    if substitution.completedAt and substitution.undoable :
                        substitution.completedAt = None
            elif execution.status in (Execution.Status.ending, Execution.Status.zombie): 
                raise Exception("The coordination phase is over!")
            else:
                assert False, execution.status
            # return the pending substitutions of the execution 
            return execution.status, execution.getPending()

    def finishExecution(self, executionId):
        with Execution.committing(self.Session, executionId) as (session, execution): #@UnusedVariable
            if execution.status in (Execution.Status.running, Execution.Status.zombie):
                pendingSubstitutions = list( substitution for substitution in execution.substitutions if substitution.completedAt is None)
                if pendingSubstitutions:
                    raise Exception("These substitutions are still pending: " + ", ".join(s.handle for s in pendingSubstitutions))
                execution.status = Execution.Status.ending
            elif execution.status == Execution.Status.backout:
                raise Exception("Cannot finish an execution being backed out")
            assert execution.status == Execution.Status.ending, execution.status 
            execution.try2Finish() 

if __name__ == "__main__":
            
    class Test3(unittest.TestCase):
        def interact(self, executionId, actionName):
            print 
            print actionName
            substitutions = self.t.getClearance(executionId, actionName)
            pprint (substitutions)
            self.t.coordinate( executionId, ("Done", substitutions.keys()) )
             
        def test(self):
            engine = create_engine('sqlite:///:memory:', echo=False)
            createDb(engine)
            Session = sessionmaker(bind=engine)
            Configuration(Session, "example.xml")
            self.t = Temporel(Session)    
            executionId1 = self.t.startExecution()
            executionId2 = self.t.startExecution()
            hNode   = self.t.declare(executionId1, 'Node'  , name="host123")
            hIp     = self.t.declare(executionId1, "Ip"    , address="1.2.3.4")
            hSubnet = self.t.declare(executionId1, "Subnet", node=hNode, net="1.2.3/24", )
            hIpNode = self.t.declare(executionId1, "IpNode", node=hNode, ip=hIp)
            self.interact(executionId1, "planNode")
            self.interact(executionId1, "planNodeProperties",)
            self.interact(executionId1, "createNode",)
            self.interact(executionId1, "setNodeProperties",)
            self.interact(executionId1, "clearNodeProperties",)
            self.interact(executionId1, "clearNode",)
            self.interact(executionId1, "delNodeProperties")
            self.interact(executionId1, "delNode")
            todo = self.t.coordinate(executionId1, ("Done", []))
            print "------"
            pprint (todo)
            self.t.backout(executionId1)
            while True:
                todo = self.t.coordinate(executionId1,
                                         ( "Undone", [handle for action in todo[1].values()
                                                           for handle in action.keys()
                                                ]
                                        ) 
                                      )
                print
                pprint (todo)
                if not todo[1]: break
            self.t.finishExecution(executionId1)
            try: todo = self.t.coordinate(executionId1, ("Done", []))
            except NoResultFound as e: print e
                
    unittest.main()
